from fastapi import FastAPI, Body, Query, Path, Cookie, Response, Header, Request
from fastapi.responses import JSONResponse
from typing import List, Dict, Optional, Union
from datetime import datetime, time, timedelta
from uuid import UUID

from starlette import status

from model import ModelName, Item, DataModel, ProjectModel, UserModel, Items, ItemDict, ItemNested, ItemListSet, Offer, \
    UserOut, UserIn, ResponseItem, ResponseItemTwo, PlaneItem, CarItem, ListItem

app = FastAPI()


"""路径参数测试接口"""


@app.get("/")
async def read_root():
    return {"Hello": "World"}


# 1.路径参数声明{item_id}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}


# 2.路径参数的预定义值
@app.get("/model/{model_name}")
async def get_model(model_name: ModelName):
    if model_name == ModelName.alexnet:
        return {"model_name": model_name, "message": "Deep Learning FTW!"}
    # value和上面一行判断一样
    if model_name.value == "lenet":
        return {"model_name": model_name, "message": "LeCNN all the images"}
    return {"model_name": model_name, "message": "Have some residuals"}


# 3.路径参数中包含文件路径
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
    return {"file_path": file_path}


# 4.路径操作的其他参数,比如标签列表(在docs和redoc中可以显示出标签下的接口)
@app.post("/items/", response_model=Item, tags=["items"])
async def create_item(*, item: Item):
    return item


@app.get("/items/", tags=["items"])
async def read_items():
    return [{"name": "Foo", "price": 42}]


@app.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "johndoe"}]


# 可以在docs和redoc中显示
@app.post("/items/", response_model=Item, summary="Create an item",
          description="Create an item with all the information, name, description, price, tax and a set of unique tags")
async def create_item(*, item: Item):
    """
    Create an item with all the information:

    - **name**: each item must have a name
    - **description**: a long description
    - **price**: required
    - **tax**: if the item doesn't have tax, you can omit this
    - **tags**: a set of unique tag strings for this item
    """
    return item


# deprecated参数表示该路径操作已过期
@app.get("/elements/", tags=["items"], deprecated=True)
async def read_elements():
    return [{"item_id": "Foo"}]


"""请求参数测试接口"""


# 1.请求参数 2.请求参数缺省值
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
# 如果函数里的参数不是路径参数的一部分，那么这样的参数就自动被解释为请求参数


# skip和limit都是有缺省值的。因此，下面URL的请求是等同的
# http://127.0.0.1:8000/fake_items/ 等同于 http://127.0.0.1:8000/fake_items/?skip=0&limit=10
@app.get("/fake_items/")
async def fake_items(skip: int = 0, limit: int = 10):
    return fake_items_db[skip: skip + limit]


# 3.可选请求参数(参数q就是可选的，缺省为None)
@app.get("/optional_item/{item_id}")
async def optional_item(item_id: str, q: str = None):
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}


# 4.多路径参数、多请求参数
# (可以同时声明多个路径参数、多个请求参数，并且不用考虑声明顺序。FastAPI可以准确无误的识别参数类型)
@app.get("/users/{user_id}/items/{item_id}")
# user_id必传参数，item_id必传参数，q是缺省参数
async def read_user_item(user_id: int, item_id: str, q: str = None, short: bool = False):
    item = {"item_id": item_id, "owner_id": user_id}
    if q:
        item.update({"q": q})
    if not short:
        item.update(
            {"description": "This is an amazing item that has a long description"}
        )
    return item


# 5.必选请求参数
# 如果一个请求参数没有被设置任何缺省值(包括None)，那么它就是必选的
@app.get("/need_items/{item_id}")
async def need_items(item_id: str, needy: str):
    item = {"item_id": item_id, "needy": needy}
    return item


# 可以声明有的参数是必选的，有的参数是有缺省值的，而有的参数是完全可选的
@app.get("/needs_items/{item_id}")
# needy是必选的，skip是有缺省值的，而limit是可选的
async def read_user_item(item_id: str, needy: str, skip: int = 0, limit: int = None):
    item = {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit}
    return item


"""单个Request Body参数测试接口"""

# 通过以上的几个环节，FastAPI就实现了以下功能：
# 1.从Request中解析出Body内容，并且输出格式为JSON。
# 2.如果需要，进行相应的数据类型转换。
# 3.校验数据，如果校验失败则返回详细错误信息。
# 4.通过声明的参数对象来接收最终数据内容


@app.post("/create_data/")
async def create_data(result: DataModel):
    item_dict = result.dict()
    if result.tax:
        price_with_tax = result.price + result.tax
        item_dict.update({"price_with_tax": price_with_tax})
    return item_dict


# 同时使用Request Body参数、路径参数、请求参数
# 函数参数按照如下的顺序进行识别匹配：
# 1.如果这个参数已经在路径中被声明过，那么它就是一个路径参数。
# 2.如果这个参数的类型是单类型的(如str、float、int、bool等)，那么它就是一个请求参数。
# 3.如果这个参数的类型是Pydantic数据模型，那么它就被认为是Request Body参数
@app.put("/update_items/{item_id}")
# item_id:路径参数,item:body参数,q:缺省参数
async def update_items(item_id: int, item: DataModel, q: str = None):
    result = {"item_id": item_id, **item.dict()}
    if q:
        result.update({"q": q})
    return result


"""多个Request Body参数测试接口"""


@app.put("/update_data/{item_id}")
async def update_data(*, item_id: int, item: ProjectModel, user: UserModel):
    results = {"item_id": item_id, "item": item, "user": user}
    return results


"""嵌入单个Request Body参数测试接口"""
"""
Request Body内容格式如下
{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    }
}
这里"item"是Request Body内部数据内容的键值，那么我们就需要利用Body方法的embed参数，才能正确解析出Request Body内容。
"""


@app.put("/update_body_item/{item_id}")
async def update_body_item(*, item_id: int, item: ProjectModel = Body(..., embed=True)):
    results = {"item_id": item_id, "item": item}
    return results


"""单个数值参数测试接口"""
# 对于Request Body里的单个数值，FastAPI提供了便利的操作方法Body


@app.put("/single_item/{item_id}")
async def single_item(*, item_id: int, item: ProjectModel, user: UserModel, importance: int = Body(...)):
    # 这里如果没有Body方法的话，参数importance就只是个请求参数，所以务必注意下
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    return results


"""1.请求参数附加信息"""

# 对请求参数附加信息的支持，FastAPI通过Query模块来实现

# 主要用于字符串参数的附加信息:
# min_length：最小长度
# max_length：最大长度
# regex：正则表达式

# 主要用于自动化文档的附加信息:
# title：参数标题
# description：参数描述信息
# deprecated：表示参数即将过期

# 特殊附加信息：
# alias：参数别名

# 可选参数声明
# q: str = Query(None)  # 等同于q: str = None
# 缺省值参数声明
# q: str = Query("query")  # 等同于q: str = "query"
# 必选参数声明
# q: str = Query(...)  # 等同于q: str


@app.get("/items_query/")
async def items_query(
    q: str = Query(
        None,
        alias="item-query",
        title="Query string",
        description="Query string for the items to search in the database that have a good match",
        min_length=3,
        max_length=50,
        regex="^fixedquery$",
        deprecated=True
    )
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results


"""2.请求参数列表"""
# FastAPI基于Query模块可以支持请求参数列表，例如请求参数q可以在URL中出现多次
# 例如http://localhost:8000/items/?q=foo&q=bar


@app.get("/q_items/")
async def q_items(q: List[str] = Query(None)):
    result = {"q": q}
    return result


# Query也支持请求参数列表的缺省值设置
@app.get("/q_items1/")
async def q_items1(q: List[str] = Query(["foo", "bar"])):
    result = {"q": q}
    return result


# 可以用list代替List[str]，但这样的话FastAPI就无法校验列表内容
@app.get("/q_items2/")
async def q_items2(q: list = Query(None)):
    result = {"q": q}
    return result


"""3.路径参数附加信息"""
# 1.导入Path模块


@app.get("/q_items_query/{item_id}")
async def q_items_query(
    # 2.添加附加信息,item_id:必选参数
    q: str, item_id: int = Path(...)
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results


"""4.路径参数、请求参数顺序问题"""
# 1、不带缺省值的参数应放在前面
# 如果把带有缺省值的参数放在了不带缺省值的参数前面，Python会发出运行警告。
# 因此在实际使用时，我们应当把不带缺省值的参数放在前面，无论这个参数是路径参数还是请求参数


@app.get("/path_items/{item_id}")
async def path_items(
    q: str, item_id: int = Path(..., title="The ID of the item to get")
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results


# 2、参数排序的技巧
# 通过传递 * 作为第一个参数，就解决了上面的参数顺序问题
@app.get("/path_items2/{item_id}")
async def path_items2(
    *, item_id: int = Path(..., title="The ID of the item to get"), q: str
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results


"""5.数字类型参数的校验"""
# 借助Query、Path等模块你可以声明对字符串类型参数的校验，同样的，也可以实现对数字类型参数的校验功能
# gt： 大于(greater than)
# ge： 大于或等于(greater than or equal)
# lt： 小于(less than)
# le： 小于或等于( less than or equal)


@app.get("/path_items_check/{item_id}")
async def path_items_check(
    *,
    item_id: int = Path(..., title="The ID of the item to get", gt=0, le=1000),
    q: str = Query(...),
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results


# 数字校验同样也适用于float类型的参数
@app.get("/float_path_items_check/{item_id}")
async def float_path_items_check(
    *,
    item_id: int = Path(..., title="The ID of the item to get", gt=0, le=1000),
    q: str = Query(...),
    size: float = Query(..., gt=0, lt=10.5),
):
    results = {"item_id": item_id, "size": size}
    if q:
        results.update({"q": q})
    return results


"""使用Pydantic模型的附加信息"""


@app.put("/update_item/{item_id}")
async def update_item(*, item_id: int, items: Items = Body(..., embed=True)):
    results = {"item_id": item_id, "items": items}
    return results


@app.post("/index_weights/")
async def create_index_weights(weights: ItemDict):
    return weights


# 直接定义一个字典类型的body，通常指定了键和值的数据类型
@app.post("/create_name/")
async def create_name(name: Dict[str, str]):
    return name


@app.put("/items_nested/{item_id}")
async def items_nested(*, item_id: int, item: ItemNested):
    results = {"item_id": item_id, "item": item}
    return results


@app.put("/item_list_set/{item_id}")
async def item_list_set(*, item_id: int, item: ItemListSet):
    results = {"item_id": item_id, "item": item}
    return results


@app.post("/item_list_set_offers/")
async def item_list_set_offers(*, offer: Offer):
    return offer


@app.put("/update_read_items/")
async def update_read_items(
    start_datetime: datetime = Body(None),
    end_datetime: datetime = Body(None),
    repeat_at: time = Body(None),
    process_after: timedelta = Body(None),
):
    start_process = start_datetime + process_after
    duration = end_datetime - start_process
    return {
        "start_datetime": start_datetime,
        "end_datetime": end_datetime,
        "repeat_at": repeat_at,
        "process_after": process_after,
        "start_process": start_process,
        "duration": duration,
    }

"""
基于Query、Path等模块同样的模式，我们可以利用Cookie模块来声明cookies。
Cookie是Query、Path的姐妹类，它们都继承自Param类
"""
# 1、导入模块

# 2、添加参数信息(参数信息的结构与Query、Path等模块完全相同)
ads_id: str = Cookie(None)


@app.get("/items/")
async def read_items(*, ads_id: str = Cookie(None)):
    return {"ads_id": ads_id}


"""返回Cookie信息给总端"""

# 1、使用Response参数
"""
可以在路径操作函数中声明Response参数，然后给这个临时的Response对象设置cookie信息。
FastAPI通过这个临时的Response对象解析出cookie信息，然后放入到最终返回的Response对象中。
可以在依赖项中使用Response参数，然后设置cookie信息
"""


@app.post("/cookie_and_object/")
def create_cookie(response: Response):
    response.set_cookie(key="data1", value="this is cookies")
    return {"message": "sets cookies"}


# 2、直接返回Response(可以在直接返回的Response对象中设置cookie信息)
@app.post("/cookie/")
async def sets_cookie():
    content = {"message": "sets cookie"}
    response = JSONResponse(content=content)
    response.set_cookie(key="data2", value="this is cookie")
    return response


"""读取Header信息"""
"""
基于Query、Path、Cookie等模块同样的模式，可以利用Header模块来声明headers
Header是Query、Path、Cookie的姐妹类，它们都继承自Param类
"""

# 1、导入模块
# from fastapi import Header
# 2、添加参数信息(参数信息的结构与Query、Path、Cookie等模块完全相同)
user_agent: str = Header(None)


@app.get("/header_items/")
async def header_items(*, user_agent: str = Header(None)):
    return {"User-Agent": user_agent}


"""3、自动转换"""
"""
大多数headers中都带有"-"字符，如"User-Agent"，但在Python中，User-Agent并不是一个合法的变量名称。
因此为了能正确提取headers中的信息，Header模块会把参数名称中的"_"转换成"-"。
Http Headers同时也是大小写不敏感的，因此可以在Python代码中使用user_agent来表示User-Agent。

"""


@app.get("/header_items1/")
async def header_items1(*, user_agent: str = Header(None)):
    return {"user_agent": user_agent}


# 可以通过设置Header中的convert_underscores参数为Flase，来禁止这种从"_"到"-"的字符转换(大多数情况下并并不需要)
@app.get("/header_items2/")
async def header_items2(*, user_agent: str = Header(None, convert_underscores=False)):
    return {"user_agent": user_agent}


# 可以用一个list来接收重复的header数据
@app.get("/header_items3/")
async def header_items3(x_token: List[str] = Header(None)):
    return {"X-Token values": x_token}


"""返回Header信息(可以在Response中返回Header信息给终端)"""
# 1、使用Response参数
# 可以在路径操作函数中声明Response参数，然后给这个临时的Response对象设置header信息。
# FastAPI通过这个临时的Response对象解析出header信息(以及cookie信息和状态码等)，然后放入到最终返回的Response对象中。
# 也可以在依赖项中使用Response参数，然后设置header信息(以及cookie信息等)。


@app.get("/headers-and-object/")
def get_headers(response: Response):
    response.headers["X-Cat-Dog"] = "alone in the world"
    return {"message": "Hello World"}


# 2、直接返回Response
@app.get("/headers/")
def headers_list():
    content = {"message": "Hello World"}
    headers = {"X-Cat-Dog": "alone in the world", "Content-Language": "en-US"}
    return JSONResponse(content=content, headers=headers)


"""通过参数status_code自定义状态码"""
# 支持任意路径的操作
# @app.get()
# @app.post()
# @app.put()
# @app.delete()


# 参数status_code接收一个数字内容，作为HTTP状态码。并且会在response中返回这个状态码，同时会在文档中记录。
# 注意status_code是装饰器方法(get，post等)的参数
@app.post("/status_code/", status_code=201)
async def status_code(name: str):
    return {"name": name}


# 可以从 fastapi.status 导入状态码常量，便于使用和记忆
@app.post("/status_codes/", status_code=status.HTTP_201_CREATED)
async def status_codes(name: str):
    return {"name": name}


"""通过Response参数自定义状态码"""
# 可以在路径操作函数中声明Response参数，然后给这个临时的Response对象设置status_code信息。
# FastAPI通过这个临时的Response对象解析出status_code信息(以及header、cookie信息等)，然后放入到最终返回的Response对象中。
# 也可以在依赖项中使用Response参数，然后设置status_code信息(以及其他信息)
tasks = {"foo": "Listen to the Bar Fighters"}


@app.put("/get-or-create-task/{task_id}", status_code=200)
def get_or_create_task(task_id: str, response: Response):
    if task_id not in tasks:
        tasks[task_id] = "This didn't exist before"
        response.status_code = status.HTTP_201_CREATED
    return tasks[task_id]


# 通过直接返回Response自定义状态码
items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}}


@app.put("/get-or-create-task2/{item_id}")
async def get_or_create_task2(item_id: str, name: Optional[str] = Body(None), size: Optional[int] = Body(None)):
    if item_id in items:
        item = items[item_id]
        item["name"] = name
        item["size"] = size
        return item
    else:
        item = {"name": name, "size": size}
        items[item_id] = item
        return JSONResponse(status_code=status.HTTP_201_CREATED, content=item)


"""直接使用Request"""


# 通过在路径操作函数中声明Request类型的参数，FastAPI就会传递Request对象给这个参数
@app.get("/request_items/{item_id}")
def request_items(item_id: str, request: Request, response: Response):
    client_host = request.client.host
    response.status_code = status.HTTP_200_OK
    return {"client_host": client_host, "item_id": item_id}


"""
1.在路径操作中，可以用参数response_model来声明Response模型
"""


# 注意response_model是装饰器方法(get，post等)的参数
@app.post("/response_model_items/", response_model=Item)
async def create_item(item: Item):
    return item


"""
2.输入输出模型示例
"""


# 然路径操作函数返回的结果是user(包含了password)，声明的Response模型是UserOut(不包含password)
# FastAPI会过滤掉所有不在输出模型中的数据，因此最终的输出结果里并没有password
@app.post("/response_model_user/", response_model=UserOut)
async def create_user(*, user: UserIn):
    return user


"""
3.Response模型参数
"""
# 1、Response模型可以有缺省值

# 2、返回实际有效数据
# 有时候只想返回被真正设置过的数据，而忽略其他未被设置过的或者缺省数据。
# 可以用参数response_model_exclude_unset来实现这个目的
item = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/response_model_item/{item_id}", response_model=ResponseItem, response_model_exclude_unset=True)
async def response_model_item(item_id: str):
    return item[item_id]

# 3、参数 response_model_include 和 response_model_exclude
# response_model_include:只返回包含在内的
# response_model_exclude:只返回包含以外的
items_two = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}


@app.get("/items/{item_id}/name", response_model=ResponseItemTwo, response_model_include={"name", "description"})
async def read_item_name(item_id: str):
    return items_two[item_id]


@app.get("/items/{item_id}/public", response_model=ResponseItemTwo, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
    return items_two[item_id]


"""
4.Response联合模型
"""

items_three = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


@app.get("/union_items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
    return items_three[item_id]


"""
5.Response列表模型
"""

list_items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/list_items/", response_model=List[ListItem])
async def list_items():
    return list_items


"""
6.Response字典模型
"""


@app.get("/keyword-weights/", response_model=Dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}
